Please enable JavaScript to view this website.

Skip to main content

Phase 2: Operational Certificate via MQTT Fleet Provisioning

Who should read these docs?

Firmware engineers implementing the fleet provisioning sequence on the device itself. This page describes the exact behavior expected by the CPP platform.

After the bootstrap certificate is installed during manufacturing (Phase 1), the device runs this sequence once on first boot to obtain its operational certificate and register itself with AWS IoT Core.

How It Works

  1. Device connects to AWS IoT Core using the bootstrap certificate (mTLS, port 8883)
  2. Device subscribes to all response topics before publishing anything
  3. Device generates a fresh RSA key pair and CSR on-device
  4. Device publishes the CSR and receives back a signed operational certificate and an ownership token
  5. Device publishes a RegisterThing request using the ownership token
  6. AWS IoT creates the device's Thing record and attaches IoT policies
  7. Device disconnects and reconnects using the operational certificate. Provisioning is complete.

Connection Requirements

Connect to the MQTT endpoint for the device's region of sale. See Prerequisites for the full endpoint table.

ParameterValue
Port8883
AuthenticationmTLS (bootstrap certificate + private key)
Client IDDevice MPBID (uppercase)
Keep Alive30 seconds
Clean Sessionfalse. The broker stores QoS 1 subscriptions across reconnects for the same client ID, so if the device disconnects between subscribing and receiving a response, the broker will redeliver rather than drop the message.
TLS SNIRequired. The endpoint uses SNI-only mode.
Server CAAmazonRootCA1; see Prerequisites
warning

The bootstrap certificate can only connect and publish/subscribe to Fleet Provisioning topics. It cannot update shadows or publish asset scan data. After this phase completes, the device must reconnect with the operational certificate for all normal operations.

Provisioning Template

The provisioning template name is device-type-specific — contact the CPP team for your product's value. Currently defined templates:

Device TypeProvisioning Template
generic-devicegeneric-provisioning-template
bridgebridge-provisioning-template

Throughout the rest of this page, {provisioning-template-name} stands in for your product's actual template name — substitute it directly into the topic strings and the templateName field.

MQTT Topics

All response topics (the accepted and rejected topics) must be subscribed to before publishing the CSR request. AWS IoT Core may deliver the response before the publish call returns, so subscribing first is not optional.

TopicDirectionQoSWhen
$aws/certificates/create-from-csr/jsonPublish1Send the CSR to request a certificate
$aws/certificates/create-from-csr/json/acceptedSubscribe1Receive the signed certificate and ownership token
$aws/certificates/create-from-csr/json/rejectedSubscribe1Receive error if certificate request fails
$aws/provisioning-templates/{provisioning-template-name}/provision/jsonPublish1Register as an IoT Thing
$aws/provisioning-templates/{provisioning-template-name}/provision/json/acceptedSubscribe1Receive confirmation; Thing created and policies attached
$aws/provisioning-templates/{provisioning-template-name}/provision/json/rejectedSubscribe1Receive error if registration fails

Certificate Ownership Token

When AWS IoT Core responds to the CSR request it includes a certificateOwnershipToken, a short-lived JWT cryptographically bound to the new certificate. This token must be included in the RegisterThing request.

The token is valid for approximately 5 minutes. The firmware must publish RegisterThing within that window after receiving the accepted response. If it expires, discard the issued certificate and restart from Step 3 (generate a new CSR).

This is why the device is responsible for calling RegisterThing rather than a backend service doing it. The token is only delivered to whoever published the CSR: the device. It proves that the entity attempting to register is the same entity that generated the certificate, not a third party that intercepted the certificate on the accepted topic. If a backend service called RegisterThing on the device's behalf, the token would have to travel from the device to the backend first, creating a new attack surface.

The pre-hook Lambda is the backend's control point; it validates the MPBID and can reject the registration, but the registration itself is intentionally device-driven so the ownership proof never has to leave the device.

What RegisterThing Does

When the device calls RegisterThing, the appropriate provisioning-template is invoked within AWS which causes the following to happen:

  • Validate the MPBID format via a pre-hook Lambda (rejects if lowercase, wrong length, or whitespace)
  • Create an IoT Thing named after the MPBID (e.g., FFFF000001)
  • Assign the Thing to the appropriate group
  • Activate the operational certificate and attaches relevant security policies to the certificate. (without the policies attached, the certificate is effectively useless)

CSR Subject Fields

The device must generate a CSR containing these X.509 subject fields exactly. The Common Name and Given Name fields are device-specific; all others are fixed values.

These requirements are the same for all CSRs regardless of type of certificate being requested.

FieldOIDValue
Common Name (CN)2.5.4.3Device type string (e.g., bridge). Contact the CPP team for your product's value.
Given Name (GN)2.5.4.42MPBID (10-character hex string) unique for each unit
Organization (O)2.5.4.10Milwaukee Tool
Organizational Unit (OU)2.5.4.11Connected Products
Country (C)2.5.4.6US
State (ST)2.5.4.8WI
Locality (L)2.5.4.7Brookfield

MQTT Payload Reference

If you are implementing Phase 2 without the AWS IoT Device SDK (e.g., a bare-metal C/C++ stack), use the raw JSON payloads below. The SDK abstracts these structures, but the topics and JSON fields are what AWS IoT Core actually processes.

CreateCertificateFromCSR

Publish to $aws/certificates/create-from-csr/json:

{
"certificateSigningRequest": "-----BEGIN CERTIFICATE REQUEST-----\nMIIC...base64...\n-----END CERTIFICATE REQUEST-----\n"
}

The CSR PEM string must preserve the \n newlines within the Base64 body (not just at the header/footer boundaries). Flatten the PEM to a single-line JSON string value with literal \n escape sequences.

Received on $aws/certificates/create-from-csr/json/accepted:

{
"certificateId": "64-character-hex-id",
"certificatePem": "-----BEGIN CERTIFICATE-----\nMIIC...base64...\n-----END CERTIFICATE-----\n",
"certificateOwnershipToken": "eyJhbGci..."
}

Store certificatePem as the operational certificate. Pass certificateOwnershipToken directly to RegisterThing; do not decode or inspect it. certificateId does not need to be stored by the firmware.

Received on $aws/certificates/create-from-csr/json/rejected:

{
"statusCode": 400,
"errorCode": "InvalidCsr",
"errorMessage": "..."
}

RegisterThing

Publish to $aws/provisioning-templates/{provisioning-template-name}/provision/json:

{
"templateName": "{provisioning-template-name}",
"certificateOwnershipToken": "eyJhbGci...",
"parameters": {
"MPBID": "FFFF000001"
}
}

MPBID must be uppercase. templateName must be your product's exact template name (see Provisioning Template); any typo returns ResourceNotFound.

Received on $aws/provisioning-templates/{provisioning-template-name}/provision/json/accepted:

{
"deviceConfiguration": {},
"thingName": "FFFF000001"
}

thingName is always the MPBID. deviceConfiguration is empty for CPP devices.

Received on $aws/provisioning-templates/{provisioning-template-name}/provision/json/rejected:

{
"statusCode": 400,
"errorCode": "InvalidRequest",
"errorMessage": "..."
}

Reference Implementation

The following Python script shows the complete Phase 2 flow using the AWS IoT Device SDK for Python. Adapt the MQTT calls to your platform's client library; the topic names and payload structures are what matters, not the SDK.

import time
from awscrt import mqtt
from awsiot import iotidentity, mqtt_connection_builder
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives import hashes, serialization
from cryptography import x509
from cryptography.x509.oid import NameOID

# --- Configuration ---
DEVICE_MPBID = "FFFF000001" # 10-char uppercase hex MPBID
MQTT_ENDPOINT = "mqtt.prod.iot.digital.milwaukeetool.com" # Match device region of sale
BOOTSTRAP_CERT_PEM = "..." # PEM string (received from Phase 1)
BOOTSTRAP_KEY_PEM = "..." # PEM string (generated on-device in Phase 1)
DEVICE_TYPE = "bridge" # Contact CPP team for your product's value

PROVISIONING_TEMPLATE = "generic-provisioning-template" # device-type-specific; see Provisioning Template section above
RESPONSE_TIMEOUT_SECS = 15


def wait_for(response: dict, operation: str):
"""Poll until a callback populates response['data'] or response['error']."""
for _ in range(RESPONSE_TIMEOUT_SECS):
if response["error"]:
raise response["error"]
if response["data"]:
return response["data"]
time.sleep(1)
raise TimeoutError(f"Timed out waiting for {operation} response")


# --- Step 1: Connect with bootstrap certificate ---
mqtt_connection = mqtt_connection_builder.mtls_from_bytes(
endpoint=MQTT_ENDPOINT,
cert_bytes=BOOTSTRAP_CERT_PEM.encode(),
pri_key_bytes=BOOTSTRAP_KEY_PEM.encode(),
client_id=DEVICE_MPBID,
clean_session=False,
keep_alive_secs=30,
)
mqtt_connection.connect().result()

identity_client = iotidentity.IotIdentityClient(mqtt_connection)

# --- Step 2: Subscribe to all response topics before publishing anything ---
create_cert_response = {"data": None, "error": None}
register_thing_response = {"data": None, "error": None}

identity_client.subscribe_to_create_certificate_from_csr_accepted(
request=iotidentity.CreateCertificateFromCsrSubscriptionRequest(),
qos=mqtt.QoS.AT_LEAST_ONCE,
callback=lambda r: create_cert_response.update({"data": r}),
)[0].result()

identity_client.subscribe_to_create_certificate_from_csr_rejected(
request=iotidentity.CreateCertificateFromCsrSubscriptionRequest(),
qos=mqtt.QoS.AT_LEAST_ONCE,
callback=lambda e: create_cert_response.update({"error": Exception(f"CreateCertificateFromCSR rejected: {e.error_code} - {e.error_message}")}),
)[0].result()

identity_client.subscribe_to_register_thing_accepted(
request=iotidentity.RegisterThingSubscriptionRequest(template_name=PROVISIONING_TEMPLATE),
qos=mqtt.QoS.AT_LEAST_ONCE,
callback=lambda r: register_thing_response.update({"data": r}),
)[0].result()

identity_client.subscribe_to_register_thing_rejected(
request=iotidentity.RegisterThingSubscriptionRequest(template_name=PROVISIONING_TEMPLATE),
qos=mqtt.QoS.AT_LEAST_ONCE,
callback=lambda e: register_thing_response.update({"error": Exception(f"RegisterThing rejected: {e.error_code} - {e.error_message}")}),
)[0].result()

# --- Step 3: Generate a new RSA key pair and CSR on-device ---
# In firmware this runs on the device. Use the subject fields from the CSR Subject Fields
# section above. CN is product-specific (contact CPP team); givenName is the MPBID.
operational_private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
operational_private_key_pem = operational_private_key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.TraditionalOpenSSL,
encryption_algorithm=serialization.NoEncryption(),
)

csr_pem = x509.CertificateSigningRequestBuilder().subject_name(x509.Name([
x509.NameAttribute(NameOID.COMMON_NAME, DEVICE_TYPE),
x509.NameAttribute(NameOID.GIVEN_NAME, DEVICE_MPBID),
x509.NameAttribute(NameOID.ORGANIZATION_NAME, "Milwaukee Tool"),
x509.NameAttribute(NameOID.ORGANIZATIONAL_UNIT_NAME, "Connected Products"),
x509.NameAttribute(NameOID.COUNTRY_NAME, "US"),
x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, "WI"),
x509.NameAttribute(NameOID.LOCALITY_NAME, "Brookfield"),
])).sign(operational_private_key, hashes.SHA256()).public_bytes(serialization.Encoding.PEM).decode()

# --- Step 4: Request the operational certificate ---
identity_client.publish_create_certificate_from_csr(
request=iotidentity.CreateCertificateFromCsrRequest(certificate_signing_request=csr_pem),
qos=mqtt.QoS.AT_LEAST_ONCE,
).result()

cert_data = wait_for(create_cert_response, "CreateCertificateFromCSR")
operational_cert_pem = cert_data.certificate_pem
ownership_token = cert_data.certificate_ownership_token
# Store operational_cert_pem and operational_private_key_pem to secure storage on the device.
# Never log the private key.
# IMPORTANT: Retain the bootstrap certificate and its private key permanently in secure storage.
# The bootstrap certificate is required for certificate rotation — it is the only credential
# that can authenticate a new CSR request when the operational certificate needs to be replaced.

# --- Step 5: Register the device as an IoT Thing ---
identity_client.publish_register_thing(
request=iotidentity.RegisterThingRequest(
template_name=PROVISIONING_TEMPLATE,
certificate_ownership_token=ownership_token,
parameters={"MPBID": DEVICE_MPBID},
),
qos=mqtt.QoS.AT_LEAST_ONCE,
).result()

thing_data = wait_for(register_thing_response, "RegisterThing")

# --- Step 6: Disconnect and reconnect with the operational certificate ---
# Unsubscribe from the four provisioning response topics before disconnecting.
# Because clean_session=False, the broker retains QoS 1 subscriptions across reconnects.
# If you skip this step, the broker may redeliver stale provisioning messages on the
# next connect (same client ID, same persistent session).
identity_client.unsubscribe("$aws/certificates/create-from-csr/json/accepted")[0].result()
identity_client.unsubscribe("$aws/certificates/create-from-csr/json/rejected")[0].result()
identity_client.unsubscribe(f"$aws/provisioning-templates/{PROVISIONING_TEMPLATE}/provision/json/accepted")[0].result()
identity_client.unsubscribe(f"$aws/provisioning-templates/{PROVISIONING_TEMPLATE}/provision/json/rejected")[0].result()

mqtt_connection.disconnect().result()

# From this point on the device uses the operational certificate for all MQTT communication.
mqtt_connection = mqtt_connection_builder.mtls_from_bytes(
endpoint=MQTT_ENDPOINT,
cert_bytes=operational_cert_pem.encode(),
pri_key_bytes=operational_private_key_pem,
client_id=DEVICE_MPBID,
clean_session=False,
keep_alive_secs=30,
)
mqtt_connection.connect().result()

print(f"Provisioning complete. Thing: {thing_data.thing_name}")

Error Reference

CreateCertificateFromCSR

Error CodeCauseResolution
InvalidCsrCSR is malformed or missing required subject fieldsRegenerate the CSR with the correct subject fields
InvalidRequestRequest payload is malformedVerify the JSON structure
UnauthorizedAccessBootstrap certificate not authorized for Fleet ProvisioningVerify the bootstrap certificate was issued correctly in Phase 1
InternalErrorAWS IoT Core internal errorRetry with exponential backoff

RegisterThing

Error CodeCauseResolution
ResourceAlreadyExistsA Thing with this MPBID already existsThe device completed Phase 2 in a prior session. If the device still has a valid operational certificate, it is already fully provisioned — skip directly to reconnecting with the operational cert. If the operational cert was lost (e.g., after a factory reset of NVM), re-provisioning a device that already has a registered Thing requires platform-side intervention; contact the CPP team.
InvalidTokenOwnership token expiredSubmit a new CSR and complete RegisterThing before the token expires
InvalidRequestPayload malformed or MPBID not uppercaseVerify MPBID is uppercase and token is present
UnauthorizedAccessCertificate not authorized for this provisioning templateVerify bootstrap certificate was issued correctly in Phase 1
ResourceNotFoundProvisioning template not foundTemplate name must exactly match your product's provisioning template (see Provisioning Template)
InternalErrorAWS IoT Core internal errorRetry with exponential backoff

Device State Machine

Ideally, firmware would track provisioning state in non-volatile memory so that a reboot during Phase 2 can resume from the correct step rather than starting over.

AWS Documentation